@thallylabs/migrate 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13876,6 +13876,73 @@ function isPassiveSvg(content) {
13876
13876
  return isPassive && sawRoot && depth === 0;
13877
13877
  }
13878
13878
 
13879
+ // src/url-asset-cache.ts
13880
+ function createMigrationAssetCache() {
13881
+ const settled = /* @__PURE__ */ new Map();
13882
+ const pending = /* @__PURE__ */ new Map();
13883
+ const remember = (url, asset) => {
13884
+ settled.delete(url);
13885
+ settled.set(url, asset);
13886
+ if (settled.size > 480) settled.delete(settled.keys().next().value);
13887
+ return asset;
13888
+ };
13889
+ const get = (url) => {
13890
+ const asset = settled.get(url);
13891
+ if (asset) return Promise.resolve(remember(url, asset));
13892
+ return pending.get(url);
13893
+ };
13894
+ return {
13895
+ remember,
13896
+ get,
13897
+ has(url) {
13898
+ return settled.has(url) || pending.has(url);
13899
+ },
13900
+ get size() {
13901
+ return settled.size + pending.size;
13902
+ },
13903
+ load(url, acquire) {
13904
+ const existing = get(url);
13905
+ if (existing) return existing;
13906
+ const request2 = Promise.resolve().then(acquire).then((asset) => {
13907
+ pending.delete(url);
13908
+ return remember(url, asset);
13909
+ }, (error) => {
13910
+ pending.delete(url);
13911
+ throw error;
13912
+ });
13913
+ pending.set(url, request2);
13914
+ return request2;
13915
+ },
13916
+ delete(url) {
13917
+ settled.delete(url);
13918
+ }
13919
+ };
13920
+ }
13921
+
13922
+ // src/url-journal-clone.ts
13923
+ function cloneUrlMigrationCheckpoint(checkpoint) {
13924
+ const ancestors = /* @__PURE__ */ new Set();
13925
+ const clone = (value) => {
13926
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
13927
+ if (typeof value === "number") return Number.isFinite(value) ? value === 0 ? 0 : value : null;
13928
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
13929
+ if (typeof value !== "object") throw new TypeError("Checkpoint must contain JSON-safe values.");
13930
+ if (ancestors.has(value)) throw new TypeError("Checkpoint must not contain circular references.");
13931
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("Checkpoint must contain plain JSON containers.");
13932
+ ancestors.add(value);
13933
+ try {
13934
+ if (Array.isArray(value)) return Array.from(value, (entry) => clone(entry) ?? null);
13935
+ return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
13936
+ const copied = clone(entry);
13937
+ return copied === void 0 ? [] : [[key, copied]];
13938
+ }));
13939
+ } finally {
13940
+ ancestors.delete(value);
13941
+ }
13942
+ };
13943
+ return clone(checkpoint);
13944
+ }
13945
+
13879
13946
  // src/url-run.ts
13880
13947
  var CONVERTER_VERSION = "url-evidence-v2";
13881
13948
  var parser2 = unified4().use(remarkParse4).use(remarkMdx4);
@@ -14250,7 +14317,8 @@ async function runUrlMigration(options) {
14250
14317
  const batchSize = Math.max(1, Math.min(500, Math.floor(options.batchSize ?? 50)));
14251
14318
  if (!Number.isFinite(concurrency) || !Number.isFinite(batchSize) || options.maxCaptures !== void 0 && (!Number.isInteger(options.maxCaptures) || options.maxCaptures < 1) || options.maxTotalBytes !== void 0 && (!Number.isFinite(options.maxTotalBytes) || options.maxTotalBytes < 1)) throw new Error("Migration batch, concurrency and byte limits must be positive finite numbers.");
14252
14319
  const captureVersion = options.captureVersion ?? "fetch-v1";
14253
- const checkpoint = options.checkpoint ? JSON.parse(JSON.stringify(options.checkpoint)) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
14320
+ if (options.checkpoint && options.checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
14321
+ const checkpoint = options.checkpoint ? cloneUrlMigrationCheckpoint(options.checkpoint) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
14254
14322
  if (checkpoint.schemaVersion !== 1 || checkpoint.converterVersion !== CONVERTER_VERSION || checkpoint.sourceUrl !== sourceUrl || checkpoint.scopePath !== scopePath || checkpoint.captureVersion !== captureVersion) throw new Error("Checkpoint does not match this source, scope or capture/converter version.");
14255
14323
  if (checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
14256
14324
  const records = /* @__PURE__ */ new Map();
@@ -14291,6 +14359,33 @@ async function runUrlMigration(options) {
14291
14359
  enqueue(sourceUrl, sourceUrl, "submitted");
14292
14360
  for (const path of ["llms.txt", "sitemap.xml", "robots.txt"]) enqueue(`${scopePath === "/" ? "" : scopePath}/${path}`, sourceUrl, "discovery-index");
14293
14361
  if (scopePath !== "/") for (const path of ["llms.txt", "sitemap.xml", "robots.txt"]) enqueue(`/${path}`, sourceUrl, "discovery-index");
14362
+ const enqueueArticleLinks = (links, base, discoveredBy) => {
14363
+ for (const link of links) {
14364
+ try {
14365
+ const url = new URL(link, base);
14366
+ if (!/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js)$/i.test(url.pathname)) enqueue(link, base, discoveredBy);
14367
+ } catch {
14368
+ }
14369
+ }
14370
+ };
14371
+ const enqueueNavigation = (nodes, base) => {
14372
+ for (const node of nodes) {
14373
+ if (node.url) enqueue(node.url, base, "source-navigation");
14374
+ enqueueNavigation(node.children ?? [], base);
14375
+ }
14376
+ };
14377
+ for (const record of [...records.values()]) {
14378
+ if (!record.evidence) continue;
14379
+ const final = canonical(record.evidence.finalUrl, sourceUrl);
14380
+ if (!final) continue;
14381
+ if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
14382
+ else if (record.outcome === "excluded") {
14383
+ if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
14384
+ for (const link of record.evidence.links) enqueue(link, final, record.url);
14385
+ }
14386
+ }
14387
+ }
14388
+ enqueueNavigation(checkpoint.navigation, sourceUrl);
14294
14389
  const fetcher = options.fetcher ?? defaultMigrationFetcher;
14295
14390
  const capture = options.captureProvider?.capture.bind(options.captureProvider) ?? (async ({ url }) => fetcher(url, { accept: ACCEPT }));
14296
14391
  const assetFetcher = options.assetFetcher ?? (!options.fetcher && !options.captureProvider ? defaultAssetFetcher : void 0);
@@ -14298,9 +14393,21 @@ async function runUrlMigration(options) {
14298
14393
  const finalUrls = new Map([...records.values()].filter((record) => record.evidence && record.page).map((record) => [canonical(record.evidence.finalUrl, sourceUrl), record.url]));
14299
14394
  let imported = usedIds.size;
14300
14395
  const assetPayloads = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.sha256, asset])));
14301
- const assetRequests = /* @__PURE__ */ new Map();
14396
+ const assetBytes = /* @__PURE__ */ new Map();
14397
+ const retainAsset = (asset) => {
14398
+ if (!acceptedAsset(asset)) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
14399
+ const sha256 = hash2(asset.content);
14400
+ const content = assetBytes.get(sha256) ?? Buffer.from(asset.content);
14401
+ assetBytes.set(sha256, content);
14402
+ return { ...asset, content };
14403
+ };
14404
+ const assetRequests = createMigrationAssetCache();
14302
14405
  for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
14303
- if (!assetRequests.has(asset.url)) assetRequests.set(asset.url, Promise.resolve({ url: asset.url, contentType: asset.contentType, content: Buffer.from(asset.base64, "base64") }));
14406
+ if (!assetRequests.has(asset.url)) {
14407
+ const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
14408
+ assetBytes.set(asset.sha256, content);
14409
+ assetRequests.remember(asset.url, { url: asset.url, contentType: asset.contentType, content });
14410
+ }
14304
14411
  }
14305
14412
  let totalBytes = checkpoint.records.reduce((sum, record) => sum + (record.evidence?.byteLength ?? 0), 0) + [...assetPayloads.values()].reduce((sum, asset) => sum + Buffer.byteLength(asset.base64, "base64"), 0);
14306
14413
  let budgetExhausted = false;
@@ -14412,10 +14519,10 @@ ${difference}`;
14412
14519
  try {
14413
14520
  let assetRequest = assetRequests.get(url);
14414
14521
  if (!suppliedAssets.has(url) && !assetRequest && assetFetcher) {
14415
- assetRequest = assetFetcher(new URL(url), options.signal);
14416
- assetRequests.set(url, assetRequest);
14522
+ assetRequest = assetRequests.load(url, () => assetFetcher(new URL(url), options.signal).then(retainAsset));
14417
14523
  }
14418
- const asset = suppliedAssets.get(url) ?? (assetRequest ? await assetRequest : null);
14524
+ const supplied = suppliedAssets.get(url);
14525
+ const asset = supplied ? retainAsset(supplied) : assetRequest ? await assetRequest : null;
14419
14526
  if (!asset) throw new Error("Capture host did not supply asset bytes or a binary fetch boundary.");
14420
14527
  const extension = acceptedAsset(asset);
14421
14528
  if (!extension) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
@@ -14432,7 +14539,7 @@ ${difference}`;
14432
14539
  const receipt = { url, path, sha256, contentType: asset.contentType, base64: existing?.base64 ?? Buffer.from(asset.content).toString("base64") };
14433
14540
  record.assets.push(receipt);
14434
14541
  if (!existing) assetPayloads.set(sha256, receipt);
14435
- if (!assetRequests.has(url)) assetRequests.set(url, Promise.resolve(asset));
14542
+ if (!assetRequests.has(url)) assetRequests.remember(url, asset);
14436
14543
  page.body = rewriteMarkdownDestinations(page.body, (destination) => canonical(destination, final) === url ? `/${path}${new URL(destination, final).hash}` : destination);
14437
14544
  } catch (error) {
14438
14545
  assetRequests.delete(url);
@@ -14465,20 +14572,8 @@ ${difference}`;
14465
14572
  checkpoint.navigation = navigation;
14466
14573
  navigationChanged = true;
14467
14574
  }
14468
- for (const link of evidence.links) {
14469
- try {
14470
- const url = new URL(link, final);
14471
- if (!/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js)$/i.test(url.pathname)) enqueue(link, final, record.url);
14472
- } catch {
14473
- }
14474
- }
14475
- const visit = (nodes) => {
14476
- for (const node of nodes) {
14477
- if (node.url) enqueue(node.url, final, "source-navigation");
14478
- visit(node.children ?? []);
14479
- }
14480
- };
14481
- visit(navigation);
14575
+ enqueueArticleLinks(evidence.links, final, record.url);
14576
+ enqueueNavigation(navigation, final);
14482
14577
  } catch (error) {
14483
14578
  if (options.signal?.aborted) return;
14484
14579
  const wasImported = record.outcome === "imported";
@@ -14548,7 +14643,13 @@ ${difference}`;
14548
14643
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
14549
14644
  validation.issues.push(...navigationIssues);
14550
14645
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
14551
- const assets = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.path, { path: asset.path, content: Buffer.from(asset.base64, "base64") }])));
14646
+ const assets = /* @__PURE__ */ new Map();
14647
+ for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
14648
+ if (!assets.has(asset.path)) {
14649
+ const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
14650
+ assets.set(asset.path, { path: asset.path, content: Buffer.from(content.buffer, content.byteOffset, content.byteLength) });
14651
+ }
14652
+ }
14552
14653
  const bundle = {
14553
14654
  sourceUrl,
14554
14655
  sourceKind: "url",